package cn.edu.seu.java.parser;

import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.FileASTRequestor;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.TypeDeclaration;


import cn.edu.seu.java.node.JavaVersion;
import cn.edu.seu.java.util.ASTNodeUtil;
import cn.edu.seu.java.util.StringUtil;

/**
 * @author Huihui Liu
 * @Time 2015-01-03
 */
public class BatchedASTParser {
	private ASTRequestor astRequestor;
	private List<String> ignoredFilePaths;
	private List<CompilationUnit> correctCompilationUnits;
	private String versionPath;

	public BatchedASTParser(String versionPath) {
		this("XX", "0.0.0", versionPath, null);
	}

	public BatchedASTParser(String projectName, String versionNum, String versionPath) {
		this(projectName, versionNum, versionPath, null);
	}

	public BatchedASTParser(String projectName, String versionNum, String versionPath, List<String> ignoredSourceFilePaths) {
		this.astRequestor = new ASTRequestor(projectName, versionNum, versionPath);
		this.versionPath = versionPath;
		this.ignoredFilePaths = ignoredSourceFilePaths;
		this.correctCompilationUnits = new LinkedList<CompilationUnit>();
	}

	class ASTRequestor extends FileASTRequestor {
		private String projectName;
		private String versionNum;
		private String versionPath;

		public ASTRequestor(String projectName, String versionNum, String versionPath) {
			this.projectName = projectName;
			this.versionNum = versionNum;
			this.versionPath = versionPath;
		}

		@Override
		public void acceptAST(String sourceFilePath, CompilationUnit node) {
			if (node.getPackage() == null) {
				//System.err.println("error:compilation unit has no or wrong package");
				//System.err.println("path:" + sourceFilePath + "\n");
			} else {
				sourceFilePath = sourceFilePath.replace("\\", "/");
				String dotSeperatedPath = sourceFilePath.replace('/', '.');
				String packageName = node.getPackage().getName().getFullyQualifiedName();

				if (dotSeperatedPath.contains(packageName) == true) {
					/*String packagePath = packageName.replace('.', '/');
					int beginIndex = sourceFilePath.indexOf(this.versionNum); // versionNum = SHA
					String fileNameEndWithJava = StringUtil.getJavaFileName(sourceFilePath);
					
					int endIndex = sourceFilePath.indexOf(packagePath + "/" + fileNameEndWithJava) - 1;
					String sourceDirectory = "";
					if (beginIndex + versionNum.length() + 1 < endIndex) {
						sourceDirectory = sourceFilePath.substring(beginIndex + versionNum.length() + 1, endIndex);
					}
					
					String relativeSourceFilePath = sourceDirectory + "/" + packagePath + "/" + fileNameEndWithJava + ".java";*/

					String relativeSourceFilePath = sourceFilePath.replace(this.versionPath, "");
					
					node.setProperty("projectName", projectName);
					node.setProperty("versionNum", versionNum);
					node.setProperty("versionPath", versionPath);
					node.setProperty("sourceFilePath", sourceFilePath); // important !!
					node.setProperty("relativeSourceFilePath", relativeSourceFilePath); // important !!
					node.setProperty("packageName", packageName); // important !!

					correctCompilationUnits.add(node);
				}
			}
		}
	}

	public List<CompilationUnit> generateCompilationUnits() {
		if (this.isDirectory(versionPath)) {
			ASTParser parser = ASTParser.newParser(AST.JLS9);

			// classpathEntries: jar files for resolving blinding, 
			String[] classpathEntries = StringUtil.getFilePathArray(versionPath, ".jar");
			String[] sourcePathEntries = { versionPath };
			parser.setEnvironment(classpathEntries, sourcePathEntries, null, true);
			parser.setResolveBindings(true);
			parser.setBindingsRecovery(true);
			parser.setStatementsRecovery(true);
			parser.setKind(ASTParser.K_COMPILATION_UNIT);

			Map<String, String> complierOptions = JavaCore.getOptions();
			JavaCore.setComplianceOptions(JavaCore.VERSION_1_8, complierOptions);
			parser.setCompilerOptions(complierOptions);

			/*
			 * parser.createASTs(String[] sourceFilePaths, String[] bindingKeys, String[] encodings
			 * FileASTRequestor requestor, IProgressMonitor monitor)
			 * String[] sourceFilePaths: paths of all *.java files in a version
			 * String[] bindingKeys
			 * String[] encodings
			 * FileASTRequestor requestor
			 * IProgressMonitor monitor
			 */
			try {
				String[] sourceFilePathArray = StringUtil.getFilePathArray(versionPath, ".java");
				// make sure each ignored file path does not contain backslashes ("\") 
				sourceFilePathArray = StringUtil.remove(sourceFilePathArray, ignoredFilePaths);

				parser.createASTs(sourceFilePathArray, null, new String[0], astRequestor, null);
			} catch (Exception e) {
				System.out.println("fail to creat ASTs in batch: " + versionPath);
			}
		}

		postProcess(); // important !!
		
		return correctCompilationUnits;
	}

	//int error = 0;
	//int total = 0;

	/**
	 * 1. extract some auxiliary information (e.g., path, qualified name for unit/class/method)
	 * 2. extract method call relation (e.g., how many methods call the given method)
	 */
	private void postProcess() {
		JavaVersion version = new JavaVersion();

		for (CompilationUnit unit : this.correctCompilationUnits) {
			String sourceFilePath = (String) unit.getProperty("sourceFilePath"); // attention !!
			collectRelatedInformation(version, unit, sourceFilePath);
		}
	}

	private void collectRelatedInformation(JavaVersion version, CompilationUnit unit, String sourceFilePath) {
		// using anonymous class fashion to visit (extract) related information
		unit.accept(new ASTVisitor() {
			@Override
			public boolean visit(CompilationUnit node) {
				node.setProperty("version", version);
				return true;
			}

			@Override
			public boolean visit(TypeDeclaration node) {
				String fullyQualifiedName = ASTNodeUtil.getFullyQualifiedName(node);
				node.setProperty("sourceFilePath", sourceFilePath); // important !!
				node.setProperty("fullyQualifiedName", fullyQualifiedName);   // important !!

				version.addClass(node);

				return true;
			}

			@Override
			public boolean visit(MethodDeclaration node) {
				String fullyQualifiedName = ASTNodeUtil.getFullyQualifiedName(node);
				node.setProperty("sourceFilePath", sourceFilePath);    // important !!
				node.setProperty("fullyQualifiedName", fullyQualifiedName); // important !!

				version.addMethod(node);

				this.extractMethodCallRelation(node);
				return false; // ignore all local methods within method's body
			}

			private void extractMethodCallRelation(MethodDeclaration caller) {
				caller.accept(new ASTVisitor() {
					@Override
					public boolean visit(MethodInvocation callee) {
						IMethodBinding calleeMethodBinding = callee.resolveMethodBinding();
						IMethodBinding callerMethodBinding = caller.resolveBinding();

						// checking null exception 
						version.addCallRelation(calleeMethodBinding, callerMethodBinding);

						return true;
					}
				});// end visiting the MI node
			}
			
		});
	}

	private boolean isDirectory(String versionPath) {
		File file1 = new File(versionPath);
		if (file1.isDirectory() != true) {
			System.err.println("Invalid Project Version Path: " + versionPath);
		}
		return file1.isDirectory();
	}
}